/******************************************************
 Laboratoire #3 : Programmation d'un serveur DNS
 
 Cours :             LOG610
 Session :           Hiver 2007
 Groupe :            01
 Projet :            Laboratoire #3
 �tudiant(e)(s) :    Maxime Bouchard
 Code(s) perm. :     BOUM24028309
 
 Professeur :        Michel Lavoie 
 Nom du fichier :    UDPReceiver.java
 Date cr�e :         2007-03-10
 Date dern. modif.   X
 *******************************************************/
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;


/**
 * Cette classe permet la r�ception d'un paquet UDP sur le port de r�ception
 * UDP/DNS. Elle analyse le paquet et extrait le hostname
 * 
 * Il s'agit d'un Thread qui �coute en permanance 
 * pour ne pas affecter le d�roulement du programme
 * @author Max
 *
 */


public class UDPReceiver extends Thread {
	/**
	 * Les champs d'un Packet UDP
	 * --------------------------
	 * En-t�te (12 octects)
	 * Question : l'adresse demand�
	 * R�ponse : l'adresse IP
	 * Autorit� : info sur le serveur d'autorit�
	 * Additionnel : information suppl�mentaire
	 */
	
	/**
	 * D�finition de l'En-t�te d'un Packet UDP
	 * ---------------------------------------
	 * Identifiant Param�tres
	 * QDcount Ancount
	 * NScount ARcount
	 * 
	 *� identifiant est un entier permettant d�identifier la requete.
	 *� parametres contient les champs suivant :
	 *	� QR (1 bit) : indique si le message est une question (0) ou une reponse (1).
	 *	� OPCODE (4 bits) : type de la requete (0000 pour une requete simple).
	 *	� AA (1 bit) : le serveur qui a fourni la reponse a-t�il autorite sur le domaine?
	 *	� TC (1 bit) : indique si le message est tronque.
	 *	� RD (1 bit) : demande d�une requete recursive.
	 *	� RA (1 bit) : indique que le serveur peut faire une demande recursive.
	 *	� UNUSED, AD, CD (1 bit chacun) : non utilises.
	 *	� RCODE (4 bits) : code de retour. 0 : OK, 1 : erreur sur le format de la requete, 2: probleme du serveur,
	 *    3 : nom de domaine non trouve (valide seulement si AA), 4 : requete non supportee, 5 : le serveur refuse
	 *    de repondre (raisons de s�ecurite ou autres).
	 * � QDCount : nombre de questions.
	 * � ANCount, NSCount, ARCount : nombre d�entrees dans les champs �Reponse�, �Autorite�, �Additionnel�.
	 */
	
	/**
	 * Les champs Reponse, Autorite, Additionnel sont tous representes de la meme maniere :
	 *
	 * � Nom (16 bits) : Pour eviter de recopier la totalite du nom, on utilise des offsets. Par exemple si ce champ
	 *   vaut C0 0C, cela signifie qu�on a un offset (C0) de 12 (0C) octets. C�est-a-dire que le nom en clair se trouve
	 *   au 12eme octet du message.
	 * � Type (16 bits) : idem que pour le champ Question.
	 * � Class (16 bits) : idem que pour le champ Question.
	 * � TTL (32 bits) : dur�ee de vie de l�entr�ee.
	 * � RDLength (16 bits): nombre d�octets de la zone RDData.
	 * � RDData (RDLength octets) : reponse
	 */
	
	private DataInputStream d = null;
	protected final static int BUF_SIZE = 1024;
	protected String SERVER_DNS = null;
	protected int port = 53;  // port de r�ception
	private String DomainName = "none";
	private String DNSFile = null;
	private String adrIP = null;
	private boolean RedirectionSeulement = false;
	private String adresseIP = null;
        private ArrayList<ConnectionInfo> connections;
	
        private class ConnectionInfo
        {
            public byte id1;
            public byte id2;
            public InetAddress ip;
            public int port;
        }
                
	public void setport(int p) {
		this.port = p;
	}
	
	public void setRedirectionSeulement(boolean b){
		this.RedirectionSeulement = b;
	}
	
	public String gethostNameFromPacket(){
		return DomainName;
	}
	
	public String getAdrIP(){
		return adrIP;
	}
	
	private void setAdrIP(String ip){
		adrIP = ip;
	}
	
	public void sethostNameFromPacket(String hostname){
		this.DomainName = hostname;
	}
	
	public String getSERVER_DNS(){
		return SERVER_DNS;
	}
	
	public void setSERVER_DNS(String server_dns){
		this.SERVER_DNS = server_dns;
	}
	
	public void UDPReceiver(String SERVER_DNS,int Port) {
		this.SERVER_DNS = SERVER_DNS;
		this.port = Port;
	}
	
	public void setDNSFile(String filename){
		this.DNSFile = filename;
	}
	
	public void run()
        {
		try
                {			
                    //*Creation d'un socket UDP
                    DatagramSocket ds = new DatagramSocket(this.port);
                    connections = new ArrayList<ConnectionInfo> ();

                    //*Boucle infinie de recpetion
                    while(true)
                    {				
                        //*Reception d'un paquet UDP via le socket
                        byte[] buf = new byte[BUF_SIZE];
                        DatagramPacket dp = new DatagramPacket(buf,BUF_SIZE);
                        ds.receive(dp);

                        //*Creation d'un DataInputStream ou ByteArrayInputStream pour manipuler les bytes du paquet
                        d = new DataInputStream (new ByteArrayInputStream(buf,0,dp.getLength()));

                        //*Lecture et sauvegarde des deux premier bytes, qui specifient l'identifiant
                        byte[] identifiant = new byte[2];
                        identifiant[0] = d.readByte();
                        identifiant[1] = d.readByte();
                        d.skipBytes(5);

                        //*Lecture et sauvegarde du huitieme byte, qui specifie le nombre de reponse dans le message 
                        byte nbReponse = d.readByte();
                        d.skipBytes(4);

                        //*Dans le cas d'une reponse
                        if (nbReponse == 0x01)
                        {
                            //*Sauvegarde du Query Domain name
                            this.DomainName = readDomainName ( d );

                            //*Passe par dessus Query Type et Query Class
                            //*Passe par dessus les premiers champs du ressource record pour arriver au ressource data
                            //*qui contient l'adresse IP associe au hostname (dans le fond saut de 16 bytes)
                            d.skipBytes(16);

                            //*Capture de l'adresse IP
                            this.adresseIP = "" + (d.readByte() & 0xff) + "." + (d.readByte() & 0xff) + "." + (d.readByte() & 0xff) + "." + (d.readByte() & 0xff);

                            //*Ajouter la correspondance dans le fichier seulement si une seule
                            //*reponse dans le message DNS (cette apllication ne traite que ce cas)
                            String queryResult = new QueryFinder ( this.DNSFile, this.DomainName).StartResearch(this.DomainName);
                            queryResult = ( queryResult == null )? "none" : queryResult;

                            if ( queryResult.equals ( "none" ) )
                                new AnswerRecorder ( DNSFile).StartRecord(DomainName, adresseIP);

                            //*Faire parvenir le paquet reponse au demandeur original, ayant emis une requete 
                            //*avec cet identifiant
                            ConnectionInfo connection = findConnection ( identifiant[0] , identifiant[1] );
                            if ( connection != null )
                            {
                                byte [] responseBuf = new UDPAnswerPacketCreator ( ).CreateAnswerPacket(buf, adresseIP);                                            
                                UDPSender sender = new UDPSender(new DatagramPacket ( responseBuf, responseBuf.length),connection.ip.getHostAddress(), connection.port);
                                sender.setSocket(ds);
                                sender.SendPacketNow(); 
                            }

                        }
                        //*Dans le cas d'une requete
                        else
                        {
                            //*Sauvegarde du Query Domain name
                            this.DomainName = readDomainName ( d );

                            //*Sauvegarde de l'adresse, du port et de l'identifiant de la requete
                            ConnectionInfo connection = findConnection ( identifiant[0] , identifiant[1] );
                            if ( connection != null )
                                connections.remove(connection);
                            connection = new ConnectionInfo ();
                            connection.id1 = identifiant[0];
                            connection.id2 = identifiant[1];
                            connection.ip = dp.getAddress();
                            connection.port = dp.getPort();                                        

                            //*Si le mode est redirection seulement
                            if (this.RedirectionSeulement)
                            {
                                connections.add ( connection );
                                //*Rediriger le paquet vers le serveur DNS
                                UDPSender sender = new UDPSender(dp,SERVER_DNS, port);
                                sender.setSocket(ds);
                                sender.SendPacketNow();                                            
                            }
                            //*Sinon <- Quel commentaire utile
                            else
                            {
                              //*Rechercher l'adresse IP associe au Query Domain name dans le fichier de
                              //*correspondance de ce serveur
                                String queryResult = new QueryFinder ( this.DNSFile, this.DomainName).StartResearch(this.DomainName);
                                queryResult = ( queryResult == null )? "none" : queryResult;

                                //*Si la correspondance n'est pas trouvee
                                if ( queryResult.equals ( "none" ) )
                                {
                                    connections.add ( connection );
                                    //*Rediriger le paquet vers le serveur DNS
                                    UDPSender sender = new UDPSender(dp,SERVER_DNS, port);
                                    sender.setSocket(ds);
                                    sender.SendPacketNow();  
                                }//*Sinon
                                else
                                {            
                                    //*Creer le paquet de reponse a l'aide du UDPAnswerPaquetCreator
                                    byte [] responseBuf = new UDPAnswerPacketCreator ( ).CreateAnswerPacket(buf, queryResult);  
                                    DatagramPacket packet = new DatagramPacket ( responseBuf, responseBuf.length);

                                    //*Placer ce paquet dans le socket
                                    UDPSender sender = new UDPSender( packet, connection.ip.getHostAddress(), connection.port);
                                    sender.setSocket(ds);

                                    //*Envoyer le paquet
                                    sender.SendPacketNow(); 
                                }
                            }
                        }
                    }
		}
                catch(Exception e)
                {
			System.err.println("Problème à l'exécution :");
			e.printStackTrace(System.err);
		}	
	}

        private String readDomainName ( DataInputStream dis )
        {
            StringBuilder domainName = new StringBuilder ( );
            
            try
            {
                byte length = dis.readByte ();
                
                while ( length != 0 )
                {
                    if ( domainName.length () > 0 )
                        domainName.append ( '.' );
                    
                    byte [] bytes = new byte [ length ];
                    dis.read ( bytes );
                    domainName.append ( new String ( bytes ) );
                    
                    length = dis.readByte ();
                }
            }
            catch ( Exception e )
            {                
            }
            
            return domainName.toString ( );            
        }
        
        private ConnectionInfo findConnection ( byte id1, byte id2 )
        {
            for ( ConnectionInfo c : connections )
                if ( c.id1 == id1 && c.id2 == id2 )
                    return c;
            return null;
        }
}

        